Skip to content

Fix bug in Bytes.lastIndexOf when array is empty and position is not 2²⁵⁶-1 #5797

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 16, 2025

Conversation

Amxx
Copy link
Collaborator

@Amxx Amxx commented Jul 15, 2025

Fix bug introduced in #5252 (v5.2)

Bug description

If the buffer is empty and if pos is not type(uint256).max

  • in the computation of Math.min(pos, length - 1) + 1, length-1 overflow and return 2²⁵⁶-1.
  • taking the min between pos and that will return something that is NOT 2²⁵⁶-1.
  • adding 1 to it will NOT return 0.
  • the loop will start at a non-0 index, and try to access data when the buffer is actually empty.

This will return unpredictable results, if s can be found after the buffer. For example, if the memory after the is clean (zero) and you do lastIndexOf('0x', '0x00', 17) ... it will return 17 instead of the expected 2²⁵⁶-1 (not found)

PR Checklist

  • Tests
  • Documentation
  • Changeset entry (run npx changeset add)

@Amxx Amxx requested a review from a team as a code owner July 15, 2025 09:18
@Amxx Amxx added the bug label Jul 15, 2025
Copy link

changeset-bot bot commented Jul 15, 2025

🦋 Changeset detected

Latest commit: 6eb86f4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Amxx Amxx requested a review from ernestognw July 15, 2025 10:25
@Amxx Amxx force-pushed the bugfix/Bytes-lastIndexOf-empty branch from 6d20d77 to edf974e Compare July 15, 2025 10:37
@Amxx Amxx added this to the 5.4 milestone Jul 15, 2025
@Amxx Amxx requested a review from gonzaotc July 15, 2025 14:28
ernestognw
ernestognw previously approved these changes Jul 15, 2025
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
ernestognw
ernestognw previously approved these changes Jul 15, 2025
@levi-sledd
Copy link

This fix looks good to me.

@vesselinux
Copy link

@ernest @hadrien cc @levi The fix LGTM. Just a nit: using the ternary operator, although correct, is not super readable IMO i.e., the logic is not immediately clear. Have you considered the following simpler syntax instead?

uint256 end;
if(pos == type(uint256).max) {
    end = length;
} else {
    end = Math.min(pos + 1, length);
}

@Amxx
Copy link
Collaborator Author

Amxx commented Jul 16, 2025

The if / else syntax generates a conditional jump that is quite expensive. So does the ternary operator c ? a : b; that I otherwise like a lot.

Math.ternary does the same using xor and mul opcode that are really cheap. That is why we favor that (branchless) syntax when possible

@@ -58,8 +58,8 @@ library Bytes {
function lastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) {
unchecked {
uint256 length = buffer.length;
// NOTE here we cannot do `i = Math.min(pos + 1, length)` because `pos + 1` could overflow
for (uint256 i = Math.min(pos, length - 1) + 1; i > 0; --i) {
uint256 end = Math.ternary(pos == type(uint256).max, length, Math.min(pos + 1, length));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be more readable, and just as effective.

Suggested change
uint256 end = Math.ternary(pos == type(uint256).max, length, Math.min(pos + 1, length));
uint256 end = Math.min(Math.saturatingAdd(pos, 1), length);

@ernestognw @vesselinux @levi-sledd WDYT ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is indeed equivalent and more readable.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Amxx Ahh, sorry to say but I find this to be even less readable haha. That's maybe because the overflow logic is buried inside the saturatingAdd thus making handling of edge cases less obvious. At the same time, I fully accept the arguments against my if-else suggestion.

The above said, I appreciate that readability is subjective, so feel free to adopt whichever of the two suggested variants the Contracts team like best. Indeed both are functionally equivalent and that's what matters at the end of the day.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the overflow logic is buried inside the saturatingAdd thus making handling of edge cases less obvious

Interesting point of view. imo "burying the logic" should be the goal of the saturatingAdd function (and generally for any other library fn). In fact, I do prefer the saturatingAdd given I'd recommend using it to other developers in a similar situation as this.

@Amxx Amxx merged commit 76e02bc into OpenZeppelin:master Jul 16, 2025
20 checks passed
@Amxx Amxx deleted the bugfix/Bytes-lastIndexOf-empty branch July 16, 2025 20:44
Amxx added a commit that referenced this pull request Jul 16, 2025
…2²⁵⁶-1 (#5797)

Co-authored-by: Ernesto García <ernestognw@gmail.com>
Signed-off-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Amxx added a commit that referenced this pull request Jul 16, 2025
…2²⁵⁶-1 (#5797)

Co-authored-by: Ernesto García <ernestognw@gmail.com>
Signed-off-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants